feat: add Readarr integration for managed downloads#191
feat: add Readarr integration for managed downloads#191juchong wants to merge 5 commits intomarkbeep:mainfrom
Conversation
Add optional Readarr support so ABR can add books to Readarr and trigger indexer searches, letting Readarr handle the full download/import/rename pipeline. When configured, ABR tries Readarr first and falls back to the direct Prowlarr download path on failure. Uses the /api/v1/search endpoint (same as pyarr) which returns book results with the full author object, avoiding the need for a separate author lookup. Books are matched by title and author name to handle cases where multiple books share the same title. New settings tab under Settings > Readarr for base URL, API key, quality/metadata profile, root folder, and search-on-add toggle. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…on_add toggle Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thank you for opening the PR. To be frank, I don't quite have the capacity to review the PR properly right now. It will be a bit before I can properly take the time to review all the changes thoroughly. Good to see that Readarr also has easy-to-use APIs. Never quite got into the details of that. |
|
Just built and deployed this branch. It seems working well so far. |
|
Thanks for this! I'm running it now and I noticed something that might be intended or not - when I add a book (that's not in my Readarr already) to Wishlist in ABR then, if the book is found in Goodreads by Readarr, ABR adds the book to Readarr but it stays in However, if I navigate to the book in ABR's Also, I can work around the Could this be because Readarr hasn't finished doing the search before the you create the book? So even though you do include Apologies if I'm misunderstanding something! Thanks again for the awesome feature. |
|
I'm not sure why the diff --git a/app/internal/readarr/client.py b/app/internal/readarr/client.py
index 2a4624a..a4e949e 100644
--- a/app/internal/readarr/client.py
+++ b/app/internal/readarr/client.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import re
+from typing import Literal
import aiohttp
from aiohttp import ClientSession
@@ -32,6 +33,7 @@ _SearchResults = TypeAdapter(list[ReadarrSearchResult])
_QualityProfiles = TypeAdapter(list[ReadarrQualityProfile])
_MetadataProfiles = TypeAdapter(list[ReadarrMetadataProfile])
_RootFolders = TypeAdapter(list[ReadarrRootFolder])
+_Book = TypeAdapter(ReadarrBook)
def _headers(session: Session) -> dict[str, str]:
@@ -82,7 +84,7 @@ async def _search(
async def _add_book(
session: Session, client_session: ClientSession, book: ReadarrBook
-) -> bool:
+) -> ReadarrBook | Literal[False]:
"""POST /api/v1/book — add a book (and its author if new) to Readarr."""
base_url = readarr_config.get_base_url(session)
if not base_url:
@@ -109,7 +111,7 @@ async def _add_book(
title=book.title,
foreignBookId=book.foreignBookId,
)
- return True
+ return _Book.validate_python(await resp.json())
except Exception as e:
logger.error("Readarr: exception adding book", error=str(e))
return False
@@ -250,10 +252,14 @@ async def readarr_add_and_search(
book.monitored = True
book.addOptions = ReadarrBookAddOptions(
- searchForNewBook=True,
+ searchForNewBook=False,
)
- return await _add_book(session, client_session, book)
+ new_book = await _add_book(session, client_session, book)
+ if new_book:
+ return await _trigger_book_search(session, client_session, new_book.id)
+
+ return not(not(new_book))
|
|
There is also an issue with the way one book can be requested in ABR and the way Readarr responds to it. I can dig up the exact example, but there are several names for the same book and Readarr could not "find" that book. When I get home I will dig up the answer. |
Adds optional Readarr integration so ABR can hand off book requests to Readarr (or compatible forks like Bookshelf) for the full download-to-library pipeline. Closes #116
What this does
When Readarr is configured in Settings, new book requests are immediately sent to Readarr as a background task. Readarr handles searching, grabbing, downloading, importing, renaming, and organizing files. If Readarr is not configured, the existing Prowlarr auto-download flow is unchanged.
query_sourcesanddownload_bookif the Readarr path fails.extra="allow"for Readarr API responses, following the existing Prowlarr typing patterns (TypeAdapter, strict basedpyright).What changed
app/internal/readarr/— new package:client.py,config.py,types.pyapp/routers/api/settings/readarr.py— API endpoints for Readarr settingsapp/routers/pages/settings/readarr.py— HTMX page routestemplates/pages/Settings/Readarr.jinja— settings UItemplates/layouts/SettingsLayout.jinja— added Readarr tabapp/routers/api/requests.py— immediate Readarr handoff increate_requestapp/internal/query.py— Readarr-first path inquery_sourcesfor manual auto-download, increased client timeout to 300sREADME.md— documentation for setup and behaviorChecks
basedpyright: 0 errors, 0 warnings, 0 notesruff format --check app: all files formatted